package com.bigflexlib.core.dependency
{
	
	
	import com.bigflexlib.utils.AutoUpdateFlag;
	import com.bigflexlib.x.flash.events.EventListenerSpec;
	
	import flash.events.Event;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	
	import mx.events.PropertyChangeEvent;
	
	
	
	
	/**
	 * 
	 * Allows you to easily implement dependency injection enforcing a simple
	 * and consistent pattern. This object is instantiated inside the owner as an internal helper.
	 * 
	 * 
	 * Dependencies are presented as public properties that can be swapped at
	 * runtime.
	 * 
	 * 
	 * 
	 * @see com.bigflexlib.utils.AutoUpdateFlag
	 * @see com.bigflexlib.core.dependency.DependencySpec
	 * 
	 * @author aldo.bucchi@gmail.com
	 * 
	 */	
	public class DependencyHelper
	{
		

		private var owner:Object;
		
		
		private var updateFunction:Function;


		private var specs:Dictionary;
		
		

		//////////////////////////////////////////////////////
		// autoUpdateFlag
		//////////////////////////////////////////////////////
		private var _autoUpdateFlag:AutoUpdateFlag;
		
		/**
		 * 
		 * The auto update flag used internally by this helper.
		 * You can call enable() and disable() to queue updates and events
		 * 
		 * @return 
		 * 
		 */
		public function get autoUpdateFlag( ):AutoUpdateFlag
		{
			return _autoUpdateFlag;
		}
		
		

		//////////////////////////////////////////////////////
		// modifications
		//////////////////////////////////////////////////////
		/**
		 * 
		 * A dictionary with all the accumulated changes since the last call to updateFunction.
		 * the key is the spec, and the value is an array with two elements ( oldValue and newValue )
		 * 
		 * Example:
		 * <pre>
		 * 
		 * dh.modifications[ wordSpec ]; // = [ "oldWord" , "newWord" ]
		 * 
		 * </pre>
		 * 
		 * 
		 * @return 
		 * 
		 */
		public function get modifications( ):Dictionary
		{
			return _autoUpdateFlag.flags;
		}





		//////////////////////////////////////////////////////
		// constructor
		//////////////////////////////////////////////////////

		
		/**
		 * 
		 * @param owner
		 *   The object that owns ( contains ) this helper and the dependencies
		 *   that is will manage
		 * 
		 * @param updateFunction
		 *   The function that will eventually be called after one ( or more )
		 *   Dependencies have been changed.
		 * 
		 * 
		 */		
		public function DependencyHelper( owner:Object, updateFunction:Function = null )
		{
			this.owner = owner;
			
			this.updateFunction = updateFunction;
			
			specs = new Dictionary( );
			
			_autoUpdateFlag = new AutoUpdateFlag( update );
		
		}
		
		
		
		
		// called by auto update flag
		private function update( ):void
		{

			// call the update function specified by the owner
			if ( updateFunction != null )
			{
				updateFunction.apply( );			
			}
			

			// inspect the flags and fire any change events
			for ( var e:* in _autoUpdateFlag.flags )
			{
				var spec:DependencySpec = e as DependencySpec;
				var values:Array = _autoUpdateFlag.flags[ spec ] as Array;
				dispatchChangeEvent( spec, values[ 0 ], values[ 1 ] );
			
			}
			
		
		}
		
		
		
				
		
		/**
		 * 
		 * Will set the new value in the internal storage and perform ( some or all ) of the following operations
		 * 
		 * <ol>
		 *   <li>If the new value is equivalent to the old value, don't do anything and return false</li>
		 *   <li>If applicable, remove event listeners from the old value ( as specified in the spec )</li>
		 *   <li>If applicable, add event listeners to the new value ( idem )</li>
		 *   <li>Request an update, which may be queued is the AutoUpdateFlag is disabled</li>
		 *   <li>Dispatch required change events after the udpate</li>
		 * <ol>
		 * 
		 * 
		 * 
		 * @param spec 
		 *   The spec of the property whose value is being changed
		 * 
		 * @param newValue 
		 *   The new value. Can be anything but should be consistent with the type of your propety.
		 * 
		 * @param consolidationDelay
		 *   Wait some time before calling in the update. Good idea when you are expecting several dependencies
		 *   To be set quasi-simultaneously ( as is the case with component initialization )
		 * 
		 * @return 
		 *   True if the value of the dependency changed after this operation.
		 * 	 If you are dispatching a change event manually, for example, use this value to see if a change has really occured.
		 * 
		 */		
		public function setDependencyValue( spec:DependencySpec , newValue:*, consolidationDelay:int=0 ):Boolean
		{
			
			if ( spec.isUsingDefault )
			{
				spec.isUsingDefault = false; // after first set, we stop using default
				spec.defaultValue = undefined; // memory considerations
			}			
			
			
			
			var oldValue:* = specs[ spec ];
			
			if ( oldValue === newValue ) // no change, dismiss
				return false;


		// set new value //////////////////////

			// replace value in our internal storage
			specs[ spec ] = newValue;


		// listeners //////////////////////////
			
			// remove old event listeners
			if ( oldValue is Object )
				modifyListeners( spec, oldValue, false );

			// add listeners to new value
			if ( newValue is Object )
				modifyListeners( spec, newValue, true );



		// store change for inspection ///////////
			
			_autoUpdateFlag.flags[ spec ] = [ oldValue, newValue ];


		// request update ////////////////////////

			if ( consolidationDelay > 0 )
				_autoUpdateFlag.disable( consolidationDelay, AutoUpdateFlag.TIME );
				
			_autoUpdateFlag.requestUpdate( );
			
			return true; // we return true because the value was effectively changed
		}
		
		
		
		
		/**
		 * 
		 * Shortcut for setDependencyValue
		 * 
		 * @param spec
		 * @param newValue
		 * @param consolidationDelay
		 * 
		 * @return 
		 * 
		 */
		public function s( spec:DependencySpec , newValue:*, consolidationDelay:int=0 ):Boolean
		{
			return setDependencyValue( spec, newValue, consolidationDelay );
		}
		
		
		
		
		
		
		
		
		
		/**
		 * 
		 * Returns the current value stored for a given dependency spec
		 * 
		 * @param spec
		 * @return 
		 * 
		 */		
		public function getDependencyValue( spec:DependencySpec ):*
		{
			
			if ( spec.isUsingDefault )
			{
				return spec.defaultValue;
			}
			
			return specs[ spec ];

		}
		
		
		public function g( spec:DependencySpec ):*
		{
			return getDependencyValue( spec );
		}
		




		private function dispatchChangeEvent( spec:DependencySpec, oldValue:*, newValue:* ):void
		{
		
			var ed:IEventDispatcher = owner as IEventDispatcher;
			
			if ( ed == null ) // cannot dispatch anything
			{
				return;			
			}
		
		
			if ( spec.changeEventType )
			{
				
				if ( ed.willTrigger( spec.changeEventType ) )
					ed.dispatchEvent( new Event( spec.changeEventType ) );			
			}		

			
			if ( spec.dispatchPropertyChangeEvent )
			{
				var pce:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent( owner, spec.property, oldValue, newValue );
				ed.dispatchEvent( pce );			
			}

		
		}




		/**
		 * 
		 * Will add/remove event listeners to a given value as specified by the spec
		 * 
		 * @param spec
		 * @param value
		 * @param subscribe
		 * 
		 */
		private function modifyListeners( spec:DependencySpec, value:*, subscribe:Boolean ):void
		{

			if ( ! value is IEventDispatcher )
				return;
				
			var d:IEventDispatcher = value as IEventDispatcher;
			
			for each ( var eventListenerSpec:EventListenerSpec in spec.eventListenerSpecs )
			{
				
				if ( subscribe )
					eventListenerSpec.addToDispatcher( d );
				else
					eventListenerSpec.removeFromDispatcher( d );
			
			}
					
		}
		

		
		
	}
	

}
